<?php
namespace Tlf;
/**
* Minimal ORM implementation.
*
*
* Declare `$protected ClassName $_article;` and `public int $article_id;` to automagically make `$obj->article` load the related article as a BigOrm object.
* @tagline (Alpha version) A minimalist ORM for mapping arrays of data to objects with magic getters & some convenience methods
*/
class BigOrm {
public string $table;
protected BigDb $db;
/**
* Create a BigOrm instance.
* @param $db a BigDb instance
*/
public function __construct(BigDb $db){
$this->db = $db;
}
/**
* Get a db-representation of your item. This is intended to convert your Orm object into a mysql-storeable array. It is NOT intended to load from the database.
*
* @return array<string, mixed>
*
* @override required because there is no default implementation
*/
public function get_db_row(): array {
throw new \RuntimeException("You must override `get_db_row():array` in your ORM class '".get_class($this)."'");
return [];
}
/**
* Initialize the Orm object from a database row
*
* @param $row array<string, mixed> a row as it would be retrieved from the database, with @key being the column name & @value being the row's value for that column.
* @return void
*
* @override required because there is no default implementation
*/
public function set_from_db(array $row){
throw new \RuntimeException("You must override `set_from_db(array \$row)` in your ORM class '".get_class($this)."'");
}
/**
* Convert binary uuid to a string uuid (mysql compatible).
* @param $uuid a binary(16) uuid from MYSQL created via `UUID_TO_BIN( UUID() )`
* @return a VARCHAR(36) compatible $uuid identical to `BIN_TO_UUID( binary_16_representation_of_uuid )`
*/
public function bin_to_uuid(string $uuid): string{
$hex = str_split(bin2hex($uuid), 4);
return
$hex[0].$hex[1].'-'
.$hex[2].'-'
.$hex[3].'-'
.$hex[4].'-'
.$hex[5].$hex[6].$hex[7]
;
// I suspect str_split approach is faster, so removed this
// $hex = bin2hex($uuid);
// return
// substr($hex,0,8).'-'
// .substr($hex,8,4).'-'
// .substr($hex,12,4).'-'
// .substr($hex,16,4).'-'
// .substr($hex,20)
// ;
}
/**
* Convert a string uuid to a binary uuid (mysql compatible).
* @param $uuid a VARCHAR(36) representation of a UUID, generated in MySql with `UUID()`
* @return a BINARY(16) representation of a UUID, generated in MySql with `BIN_TO_UUID( UUID() )`
*/
public function uuid_to_bin(string $uuid): string{
$clean = str_replace('-','', $uuid);
return hex2bin($clean);
}
/**
* Convert a mysql-stored datetime string to a PHP DateTime instance
* @param $mysql_datetime mysql-stored datetime string
* @return DateTime instance
*/
public function str_to_datetime(string $mysql_datetime): \DateTime {
return \DateTime::createFromFormat('Y-m-d H:i:s', $mysql_datetime);
}
/**
* Convert a PHP DateTime object into a mysql DATETIME string
* @param $datetime a DateTime object
* @return a string compatible with MySql's DATETIME type
*/
public function datetime_to_str(\DateTime $datetime): string {
return $datetime->format('Y-m-d H:i:s');
}
/**
* Call and return the property getter. For `$prop = 'author'`, call `$this->getAuthor()`
*
* @param $prop a property name
* @return the value from the property getter.
*/
public function __get(string $prop): mixed {
$method = 'get'.ucfirst($prop);
return $this->$method();
}
/**
* Call the property setter. For `$prop = 'author'`, call `$this->setAuthor($value)`
*
* @param $prop a property name
* @param $value the value to set
* @return void
*/
public function __set(string $prop, mixed $value){
$method = 'set'.ucfirst($prop);
$this->$method($value);
}
/**
* Store the item in the database. If `is_saved()` returns `true`, then use an UPDATE, else use an INSERT.
* UPDATEs are performed based on the `int $id` property of the Orm object, assuming an `id int PRIMARY KEY AUTO_INCREMENT` db column.
*
* @override if your table does not use a primary key, autoincrement `id`, or if your auto increment column has a different name.
* @return int id of the item's db row
*/
public function save(): int {
$row = $this->get_db_row();
$row = $this->onWillSave($row);
if ($this->is_saved()){
$this->db->update($this->table(), ['id'=>$this->id], $row);
} else {
$this->id = $this->db->insert($this->table(), $row);
}
$this->onDidSave($row);
return $this->id;
}
/**
* Delete this item from the database, where the db column `id` matches this item's property `id`
*
* @override if db column `id` is not your unique primary key OR if `$this->id` does not correspond to database column `id`.
* @return `true` if the item was deleted, `false` otherwise. `false` if there is an error or if this item is not already saved in the db.
*/
public function delete(): bool {
if ($this->is_saved()){
$db_row = $this->get_db_row();
if (!$this->onWillDelete($db_row))return false;
$did_delete = $this->db->delete($this->table(), ['id'=>$this->id]);
if ($did_delete){
unset($this->id);
$this->onDidDelete($db_row);
return true;
}
else return false;
} else {
return false;
}
}
/**
* Refreshes this item, so it matches what's in the database. Just queries for this item's row (by id), then calls `$this->set_from_db($row)`.
*
* @throw RuntimeException if `$this->id` is not set, or if no rows are returned, or if more than one row is returned.
* @return the old db row, as gotten from `$this->get_db_row()`
*
* @override to refresh based on a property/column other than `id`, or if you want different error handling than exceptions.
*/
public function refresh(): array {
$old_row = $this->get_db_row();
if (!isset($this->id)){
throw new \RuntimeException("Cannot refresh. This item's `id` is not set, so it cannot be refreshed. Class '".get_class($this)."' can override `refresh()` if `id` is not the reference property.");
}
$rows = $this->db->select($this->table(), ['id'=>$this->id]);
if (count($rows)==0){
throw new \RuntimeException("Cannot refresh. Could not find a row with id '".$this->id."' in table '".$this->table()."'");
}
if (count($rows)>1){
throw new \RuntimeException("Cannot refresh. Multiple rows returned with id '".$this->id."' in table '".$this->table()."'");
}
$this->set_from_db($rows[0]);
return $old_row;
}
/**
* Check if the current item is already stored in the database. Default implementation returns true if `id` property isset & is > 0
*
* @override if the `id` property/column is not reliable for determining whether your item already exists in the database.
* @return true if the item is already in the database, false otherwse
*/
public function is_saved(): bool{
return isset($this->id) && $this->id > 0;
}
/**
* Get the table name. Default implementation return `$this->table` or the lowercase version of the class name if `$this->table` is null
*
* @override if you are not setting the `table` property AND your class's basename does not map to the table's name in the database.
* @return database table name
*/
public function table(): string {
if (isset($this->table))return $this->table;
$parts = explode('\\', strtolower(get_class($this)));
$class = array_pop($parts);
return $class;
}
/**
* Hook called before an item is saved. Returns the correct row to save.
*
* @param $row array<string, mixed> the array returned by `get_db_row()`
* @return array<string, mixed> the correct row to save to database
*
* @override if you need to modify `$row` prior to INSERT/UPDATE, or if you need to do something else prior to db storage.
*/
public function onWillSave(array $row): array {
return $row;
}
/**
* Hook called after an item is saved.
*
* @param $row array<string, mixed> the row that was used for INSERT/UPDATE, typically same as `get_db_row()`, or a modified copy returned by `onWillSave()`
* @return void
*
* @override if you need to query values auto-generated by mysql, or if you need to perform other actions after INSERT/UPDATE
*/
public function onDidSave(array $row) {
}
/**
* Hook called before an item is deleted.
*
* @param $row array<string, mixed> the array returned by `get_db_row()`
* @return `false` to stop deletion or `true` to continue.
*
* @override If there are cases where you want to prevent deletion of an item. Cleanup should go in `onDidDelete(array $row)`, but pre-steps should go here. Ex: Article can only be deleted if its tags are deleted first. Delete tags during onWillDelete & if they fail to delete, then return `false` to prevent article deletion.
*/
public function onWillDelete(array $row): bool {
return true;
}
/**
* Hook called after an item is deleted from database.
*
* @param $row array<string, mixed> the row that was deleted. This is gotten from `$this->get_db_row()` before the deletion, NOT from the database
* @return void
*
* @override if you need to do some cleanup after deletion
*/
public function onDidDelete(array $row) {
}
}